#!/usr/bin/python -u

'''Brultech ECM-1240 Data POSTer.
    This code was written and modified by the following:
    Amit Snyderman <amit@amitsnyderman.com>
    bpwwer & tenholde from the cocoontech.com forums
    Kelvin Kakugawa
    brian jackson [http://fivejacksons.com/brian/]
    Marc MERLIN <marc_soft@merlins.org> | http://marc.merlins.org/perso/solar/
    Brad Norman <brad@greenhomegear.com>
    
    The code receives binary energy packets from a Brultech ECM-1240 via
    a TCP/IP socket, decodes the data, opens TCP/IP socket and POSTs the data to
    an online energy dashboard and analytics company.
    
    The code needs to be modified to differentiate data from several ECMs.
    ECM data needs to be stored based on the "unit-id" for both "now" and
    "prev" data.  Intuitively it appears I need help modifying classes,
    MovingBuffer and BaseProcessor.  25 years ago I understood procedural
    code.  OOP is new to me.  I wish to understand this code.
'''

import urllib
import pycurl
import StringIO
import base64
import bisect
import new
import optparse
import socket
import sys
import time
import traceback
import urllib
import urllib2

# External (Optional) Dependencies
try:
    import serial
except Exception, e:
    serial = None

MINUTE  = 60
HOUR    = 60 * MINUTE
DAY     = 24 * HOUR

# PACKET SETTINGS
START_HEADER0    = hex(254)
START_HEADER1    = hex(255)
START_HEADER2    = hex(3)

DATA_BYTES_LENGTH = 62
SEC_COUNTER_MAX   = 16777216

# SERIAL SETTINGS
SERIALPORT  = "COM5"            # the com/serial port the ecm is connected to (COM4, /dev/ttyS01, etc)
BAUDRATE    = 19200             # the baud rate we talk to the ecm

# Helper Functions

def calculate(now, prev):
    '''calc average watts/s between packets'''
    if now['secs'] < prev['secs']:
        now['secs'] += SEC_COUNTER_MAX # handle seconds counter overflow
    secs_delta = float(now['secs'] - prev['secs'])

    # CH1/2 Watts
    now['ch1_watts'] = (now['ch1_aws'] - prev['ch1_aws']) / secs_delta
    now['ch2_watts'] = (now['ch2_aws'] - prev['ch2_aws']) / secs_delta

    now['ch1_positive_watts'] = (now['ch1_pws'] - prev['ch1_pws']) / secs_delta
    now['ch2_positive_watts'] = (now['ch2_pws'] - prev['ch2_pws']) / secs_delta

    now['ch1_negative_watts'] = now['ch1_watts'] - now['ch1_positive_watts']
    now['ch2_negative_watts'] = now['ch2_watts'] - now['ch2_positive_watts']

    # All Watts increase no matter which way the current is going
    # Polar Watts only increase if the current is positive
    # Every Polar Watt does register as an All Watt too.
    # math comes to: Watts = 2x Polar Watts - All Watts
    now['ch1_pwh']  = now['ch1_pws'] / 3600000.0
    now['ch2_pwh']  = now['ch2_pws'] / 3600000.0
    now['ch1_nwh']  = (now['ch1_aws'] - now['ch1_pws']) / 3600000.0
    now['ch2_nwh']  = (now['ch2_aws'] - now['ch2_pws']) / 3600000.0
    now['ch1_wh']   = now['ch1_pwh'] - now['ch1_nwh']
    now['ch2_wh']   = now['ch2_pwh'] - now['ch2_nwh']


    now['aux1_wh'] = now['aux1_ws'] / 3600000.0
    now['aux2_wh'] = now['aux2_ws'] / 3600000.0
    now['aux3_wh'] = now['aux3_ws'] / 3600000.0
    now['aux4_wh'] = now['aux4_ws'] / 3600000.0
    now['aux5_wh'] = now['aux5_ws'] / 3600000.0


    # Polar Watts' instant value's only purpose seems to help find out if
    # main watts are positive or negative. Polar Watts only goes up if the
    # sign is positive. If they are null, that means the sign is negative.
    if (now['ch1_positive_watts'] == 0):
        now['ch1_watts'] *= -1 
    if (now['ch2_positive_watts'] == 0):
        now['ch2_watts'] *= -1 

    # AUX1-5 Watts
    now['aux1_watts'] = (now['aux1_ws'] - prev['aux1_ws']) / secs_delta
    now['aux2_watts'] = (now['aux2_ws'] - prev['aux2_ws']) / secs_delta
    now['aux3_watts'] = (now['aux3_ws'] - prev['aux3_ws']) / secs_delta
    now['aux4_watts'] = (now['aux4_ws'] - prev['aux4_ws']) / secs_delta
    now['aux5_watts'] = (now['aux5_ws'] - prev['aux5_ws']) / secs_delta

    now['time'] = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime())


# Packet Server Classes

class BasePacketServer(object):
    def __init__(self, packet_processor):
        self.packet_processor = packet_processor

    def _convert(self, bytes):
        ''' convert binary watt/second counters to integers '''        
        return reduce(lambda x,y:x+y[0] * (256**y[1]), zip(bytes,xrange(len(bytes))),0)

    def _compile(self, packet):
        ''' decode the 57 byte data packet '''
        now = {}

        # Voltage Data (2 bytes)
        now['volts'] = 0.1 * self._convert(packet[1::-1])

        # CH1-2 Absolute Watt-Second Counter (5 bytes each)
        now['ch1_aws'] = self._convert(packet[2:7])
        now['ch2_aws'] = self._convert(packet[7:12])

        # CH1-2 Polarized Watt-Second Counter (5 bytes each)
        now['ch1_pws'] = self._convert(packet[12:17])
        now['ch2_pws'] = self._convert(packet[17:22])

        # Reserved (4 bytes)

        # Device Serial Number (2 bytes)
        now['ser_no'] = self._convert(packet[27:25:-1])

        # Reset and Polarity Information (1 byte)
        now['flag'] = self._convert(packet[28:29])

        # Device Information (1 byte)
        now['unit_id'] = self._convert(packet[29:30])

        # CH1-2 Current (2 bytes each)
        now['ch1_amps'] = 0.01 * self._convert(packet[30:32])
        now['ch2_amps'] = 0.01 * self._convert(packet[32:34])

        # Seconds (3 bytes)
        now['secs'] = self._convert(packet[34:37])

        # AUX1-5 Watt-Second Counter (4 bytes each)
        now['aux1_ws'] = self._convert(packet[37:41])
        now['aux2_ws'] = self._convert(packet[41:45])
        now['aux3_ws'] = self._convert(packet[45:49])
        now['aux4_ws'] = self._convert(packet[49:53])
        now['aux5_ws'] = self._convert(packet[53:57])

        return now

    def process(self, packet):
        packet = self._compile(packet)

        self.packet_processor.preprocess(packet)
        self.packet_processor.process(packet)

    def read(self):
        pass
    
    def run(self):
        ''' main loop to read RS-232 packets '''
        try:
            self.packet_processor.setup()
            global Payload
            Payload = []
            while True:
                try:
                    self.read()

                except Exception, e:
                    if type(e) == KeyboardInterrupt: # only break for KeyboardInterrupt
                        raise e

                    traceback.print_exc()
                    if not self.packet_processor.handle(e):
                        print 'Exception [in %s]: %s' % (self, e)

        except Exception, e:
            traceback.print_exc()
            sys.exit(1)

        finally:
            self.packet_processor.cleanup()

class SerialPacketServer(BasePacketServer):
    def __init__(self, packet_processor, port=SERIALPORT, baudrate=BAUDRATE):
        super(SerialPacketServer, self).__init__(packet_processor)

        self._port        = port
        self._baudrate    = baudrate

        if not serial:
            print 'Error: pySerial module could not be imported.'
            sys.exit(1)

        self.conn = None

    def read(self):
        ''' read RS-232 ECM-1240 data '''
        global Payload
        try:
            self.conn = serial.Serial(self._port, self._baudrate)
            self.conn.open()

            while True:
                data = self.conn.read(1)
                if not data:
                    break
                    
                # determine start of new packet
                header_byte0 = hex(ord(data))
                if header_byte0 != START_HEADER0:
                    continue

                data = self.conn.read(1)
                header_byte1 = hex(ord(data))
                if header_byte1 != START_HEADER1:
                    continue

                data = self.conn.read(1)
                header_byte2 = hex(ord(data))
                if header_byte2 != START_HEADER2:
                    continue
                    
                # gather packet data
                packet = self.conn.read(DATA_BYTES_LENGTH)
                while len(packet) < DATA_BYTES_LENGTH:
                    data = self.conn.read(DATA_BYTES_LENGTH-len(packet))
                    if not data: # No data left
                        raise Exception('Received no data.')
                    packet += data
                packet = [ord(c) for c in packet]
                if packet[29:30] == [3]:    #bn blocks packets from other ECMs
                    continue                #bn
                    
                # *** THIS IS WHERE THE CODE NEEDS TO BE MODIFIED
                # packet[29:30] is the "unit id" which indicates 
                # which ECM unit you are receiving data from.
                # When calling self.process, a unit id needs to be
                # passed also.
                self.process(packet)
                listcount = len(Payload)
                
                # this section added by bn.  it POSTs data after 10 packets
                # are processed using a direct socket method
                if listcount == 10:
                    xmltop = "<upload>\r\n <sensor>\r\n"
                    Payload.insert(0,xmltop)
                    xmlbottom = " </sensor>\r\n</upload>\r\n\r\n"
                    Payload.append(xmlbottom)
                    xmlstring = ''.join(Payload)
                    Payload = []
                    print
                    ContentLen = len(xmlstring)
                    Request =       "POST /fetcher/data HTTP/1.1\r\n"
                    Token =         "Token: insert-Token-here\r\n"
                    Connection =    "Connection: Close\r\n"
                    ContentType =   "Content-Type: application/xml\r\n"
                    UserAgent =     "User-Agent: Python/v2.6\r\n"
                    Host =          "Host: data.myenersave.com\r\n"
                    Accept =        "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"
                    ContentLen =    "Content-Length: %s\r\n" % ContentLen
                    BlankLine =     "\r\n";
                    EnerSaveTX = Request + Token +  Connection +  ContentType +  UserAgent + Host + Accept + ContentLen  + BlankLine + xmlstring
                    print EnerSaveTX

                    HOST = '50.19.223.151'
                    PORT = 80
 
                    try:
                        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    except socket.error, msg:
                        sys.stderr.write("[ERROR] %s\n" % msg[1])
                        sys.exit(1)
 
                    try:
                        sock.connect((HOST, PORT))
                    except socket.error, msg:
                        sys.stderr.write("[ERROR] %s\n" % msg[1])
                        sys.exit(2)

                    sock.send(EnerSaveTX)
 
                    data = sock.recv(1024)
                    string = ""
                    while len(data):
                        string = string + data
                        data = sock.recv(1024)
                    sock.close()
                    print string

        finally:
            if self.conn:
                self.conn.close()
                self.conn = None

# Packet Processor Classes

class MovingBuffer(object):
    '''Maintain fixed-size buffer of data over time'''
    def __init__(self, max_timeframe=DAY):
        self.time_points    = []
        self.max_timeframe    = max_timeframe

    def insert(self, timestamp, time_dict):
        bisect.insort(self.time_points, (timestamp, time_dict))

        now = int(time.time())
        cull_index = bisect.bisect(self.time_points, (now-self.max_timeframe, {}))
        del(self.time_points[:cull_index])

    def data_over(self, time_delta):
        now = int(time.time())
        delta_index = bisect.bisect(self.time_points, (now-time_delta, {}))

        return self.time_points[delta_index:]

    def delta_over(self, time_delta):
        now = int(time.time())
        delta_index = bisect.bisect(self.time_points, (now-time_delta, {}))

        offset = self.time_points[delta_index][1]
        current = self.time_points[-1][1]

        calculate(current, offset)
        return current

class BaseProcessor(object):
    def __init__(self, buffer_timeframe, *args, **kwargs):
        self.buffer    = MovingBuffer(buffer_timeframe)

    def setup(self):
        pass

    def preprocess(self, packet):
        self.buffer.insert(int(time.time()), packet)

    def process(self, packet):
        pass
    
    def handle(self, exception):
        return False

    def cleanup(self):
        pass

class PrintMixin(object):
    def __init__(self, *args, **kwargs):
        super(PrintMixin, self).__init__(*args, **kwargs)
        
        self.print_out = kwargs.get('print_out', False)
        self.prev_packet = {}

    def process(self, packet):
        ''' format data payload 
        this section was completely changed by bn '''
        if self.prev_packet:
            calculate(packet, self.prev_packet)
            epoch = "%.0f" % (time.time())
            wh = "%.1f" % (packet['ch1_aws']/3600)
            whdata = "  <energy time=\"%s\" reg=\"%s\"/>\r\n" % (epoch, wh)
            Payload.append(whdata)

        self.prev_packet = packet
        super(PrintMixin, self).process(packet)        

if __name__ == '__main__':
    buffer_timeframe = 5*MINUTE
    bases = [BaseProcessor]
    bases.insert(0, PrintMixin)
    Processor = new.classobj('Processor', tuple(bases), {})
    processor = Processor(buffer_timeframe)
    server = SerialPacketServer(processor)
    server.run()
    sys.exit(0)

